﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.ServiceProcess;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Autofac;
using uTILLIty.Rave.Contract.Exception;
using uTILLIty.Rave.Contract.Job;
using uTILLIty.Rave.Core;
using uTILLIty.Rave.Core.Manager;
using uTILLIty.Rave.Service.Properties;

namespace uTILLIty.Rave.Service
{
	public partial class DirCreatorService : ServiceBase
	{
		private readonly JobManager _jobManager;
		internal readonly TraceSource TraceSource = new TraceSource("DirCreator");
		private Timer _retryTimer;
		private MergeVariable[] _globalMergeVariables;

		public DirCreatorService(JobManager jobManager)
		{
			_jobManager = jobManager;
			InitializeComponent();
		}

		private PerformanceCounter _jobStartPerfCounter;
		private PerformanceCounter _jobEndPerfCounter;
		private PerformanceCounter _jobErrorPerfCounter;
		private PerformanceCounter _jobsRunningPerfCounter;

		private void CreateCounters(DirectoryInfo dir)
		{
			_jobStartPerfCounter = new PerformanceCounter();
			_jobEndPerfCounter = new PerformanceCounter();
			_jobErrorPerfCounter = new PerformanceCounter();
			_jobsRunningPerfCounter = new PerformanceCounter();

			_jobStartPerfCounter.BeginInit();
			_jobEndPerfCounter.BeginInit();
			_jobErrorPerfCounter.BeginInit();
			_jobsRunningPerfCounter.BeginInit();

			_jobStartPerfCounter.CounterName = "JobStart";
			_jobEndPerfCounter.CounterName = "JobEnd";
			_jobErrorPerfCounter.CounterName = "Error";
			_jobsRunningPerfCounter.CounterName = "JobsRunning";

			var counters = new[] { _jobEndPerfCounter, _jobStartPerfCounter, _jobErrorPerfCounter, _jobsRunningPerfCounter };
			foreach (var counter in counters)
			{
				counter.ReadOnly = false;
				counter.InstanceLifetime = PerformanceCounterInstanceLifetime.Process;
				counter.InstanceName = !string.IsNullOrWhiteSpace(Settings.Default.ServiceNameSuffix)
					? Settings.Default.ServiceNameSuffix
					: dir.FullName;
				counter.CategoryName = "DirCreator";
				counter.RawValue = 0;

				counter.EndInit();
			}
		}

		protected override void OnStart(string[] args)
		{
			StartService();
		}

		protected override void OnStop()
		{
			StopService();
		}

		protected override void OnContinue()
		{
			StartService();
		}

		protected override void OnPause()
		{
			StopService();
		}

		public void StartService()
		{
			FileVersionInfo fileInfo = FileVersionInfo.GetVersionInfo(typeof(Program).Assembly.Location);
			FileVersionInfo coreFileInfo = FileVersionInfo.GetVersionInfo(typeof(IocModule).Assembly.Location);
			TraceEvent(WellKnownEventIds.AppStartup, Resources.AppStartup,
								 fileInfo.ProductName, coreFileInfo.ProductVersion);

			ApplyConfiguration();
			jobHotfolderWatcher.EnableRaisingEvents = true;

			var configFile = new FileInfo(AppDomain.CurrentDomain.SetupInformation.ConfigurationFile);
			configWatcher.Path = configFile.Directory != null ? configFile.Directory.FullName : Environment.CurrentDirectory;
			configWatcher.Filter = configFile.Name;
			configWatcher.EnableRaisingEvents = true;

			TraceEvent(WellKnownEventIds.BeginWatchingDirectory, Resources.BeginWatchingDirectory,
								 jobHotfolderWatcher.Path,
								 jobHotfolderWatcher.IncludeSubdirectories, jobHotfolderWatcher.Filter);

			new Task(ProcessExistingFiles).Start();
		}

		private void StopService()
		{
			_retryTimer.Dispose();
			_retryTimer = null;
			jobHotfolderWatcher.EnableRaisingEvents = false;
			configWatcher.EnableRaisingEvents = false;

			TraceEvent(WellKnownEventIds.EndWatchingDirectory, Resources.EndWatchingDirectory,
								 jobHotfolderWatcher.Path);
		}

		private void ApplyConfiguration()
		{
			if (!string.IsNullOrWhiteSpace(Settings.Default.TemplatesRoot))
				EnsureDirectoryExists(Settings.Default.TemplatesRoot);

			InitializeHotfolder(Settings.Default.Hotfolder);
			_retryTimer = new Timer(OnRetry, null, 500, Settings.Default.RetryIntervalSec * 1000);

			_globalMergeVariables = ParseMergeVariables(Settings.Default.MergeVariables);
		}

		private static readonly Regex MergeVariable = new Regex(@"(?in)(?<key>[^=]+)=(?<value>.*)$", RegexOptions.Compiled);
		protected internal MergeVariable[] ParseMergeVariables(IList mergeVariables)
		{
			var list = new List<MergeVariable>(mergeVariables.Count);
			foreach (string line in mergeVariables)
			{
				if (string.IsNullOrEmpty(line))
					continue;
				if (line.StartsWith("#"))
					continue;

				var match = MergeVariable.Match(line);
				if (!match.Success)
					throw new ConfigurationErrorException(Resources.InvalidMergeVariable, line);

				var key = match.Groups["key"].Value;
				var value = match.Groups["value"].Value;
				list.Add(new MergeVariable(key, value));
			}
			return list.ToArray();
		}

		private void ProcessExistingFiles()
		{
			var path = new DirectoryInfo(jobHotfolderWatcher.Path);
			var options = jobHotfolderWatcher.IncludeSubdirectories
				? SearchOption.AllDirectories
				: SearchOption.TopDirectoryOnly;

			foreach (var file in path.GetFiles(jobHotfolderWatcher.Filter, options)
				.Where(f => (f.Attributes & FileAttributes.Hidden) == 0))
			{
				ProcessJobAsync(file.FullName);
			}
		}

		private void InitializeHotfolder(string folder)
		{
			var dir = EnsureDirectoryExists(folder);
			CreateCounters(dir);
			jobHotfolderWatcher.Path = dir.FullName;
			jobHotfolderWatcher.IncludeSubdirectories = Settings.Default.IncludeSubdirectories;
			jobHotfolderWatcher.Filter = Settings.Default.Filter;
		}

		private static DirectoryInfo EnsureDirectoryExists(string folder)
		{
			var dir = new DirectoryInfo(Environment.ExpandEnvironmentVariables(folder));
			if (!dir.Exists)
				dir.Create();
			return dir;
		}

		readonly Hashtable _currentFiles = new Hashtable(10);
		private readonly object _currentFilesLock = new object();

		private void OnJobFileCreated(object sender, FileSystemEventArgs e)
		{
			HoldForDeleyIfSet();
			ProcessJobAsync(e.FullPath);
		}

		private void OnJobFileRenamed(object sender, RenamedEventArgs e)
		{
			HoldForDeleyIfSet();
			ProcessJobAsync(e.FullPath);
		}

		private static void HoldForDeleyIfSet()
		{
			if (Settings.Default.ProcessingDelayMs > 0)
				Thread.Sleep(Settings.Default.ProcessingDelayMs);
		}

		/// <summary>
		/// Queues a job for processing (in another thread)
		/// </summary>
		/// <param name="fileName">The job file to process</param>
		/// <seealso cref="ProcessJob"/>
		public void ProcessJobAsync(string fileName)
		{
			new Task(state => ProcessJob((string)state), fileName).Start();
		}

		private void FinishedProcessingFile(object fullPath)
		{
			lock (_currentFilesLock)
			{
				if (_currentFiles.Contains(fullPath))
					_currentFiles.Remove(fullPath);
			}
		}

		private bool IsFileAlreadyBeingProcessed(object fullPath)
		{
			lock (_currentFilesLock)
			{
				if (_currentFiles.Contains(fullPath))
					return true;
				_currentFiles.Add(fullPath, fullPath);
			}
			return false;
		}

		/// <summary>
		/// Processes the job specified
		/// </summary>
		/// <param name="jobFile">The job file to process</param>
		public void ProcessJob(string jobFile)
		{
			//usually lock-files - ignore these!
			if (jobFile.StartsWith("~") || (new FileInfo(jobFile).Attributes & FileAttributes.Hidden) != 0)
				return;

			if (IsFileAlreadyBeingProcessed(jobFile))
				return;

			Job job = null;
			try
			{
				_jobStartPerfCounter.Increment();
				_jobsRunningPerfCounter.Increment();
				TraceEvent(WellKnownEventIds.ProcessingJob, Resources.ProcessingJob, jobFile);

				Stream stream = null; //used for locking the file until it's been processed
				try
				{
					job = LoadJob(jobFile, out stream);
					if (job == null)
						return; //file locked or moved
					ExecuteJob(job);
				}
				finally
				{
					if (stream != null)
						stream.Dispose();
				}
				job.TraceSource.Close();
				Thread.Sleep(50); //allow file-handles to close
				CleanupSuccessfulJob(job);
			}
			catch (Exception ex)
			{
				if (job == null)
				{
					job = new Job { JobSource = jobFile, LogLevel = SourceLevels.Error };
					_jobManager.InitializeLogging(job);
				}
				_jobErrorPerfCounter.Increment();
				TraceEvent(WellKnownEventIds.JobError, Resources.JobError, jobFile, ex.Message);
				CleanupAndLogFailedJob(job, ex);
			}
			finally
			{
				_jobEndPerfCounter.Increment();
				_jobsRunningPerfCounter.Decrement();
				TraceEvent(WellKnownEventIds.JobCompleted, Resources.JobCompleted, jobFile, job != null ? job.JobId : "[null]");
				FinishedProcessingFile(jobFile);
			}
		}

		private Job LoadJob(string jobFile, out Stream stream)
		{
			try
			{
				stream = File.Open(jobFile, FileMode.Open, FileAccess.Read, FileShare.None);
				var job = _jobManager.LoadJob(stream, jobFile);
				job.JobSource = jobFile;
				AdjustTemplatePath(job);
				InjectMergeVariables(job);
				return job;
			}
			catch (IOException ex)
			{
				TraceEvent(WellKnownEventIds.JobLockedOrMoved, Resources.JobLockedOrMoved, jobFile, Settings.Default.RetryIntervalSec , ex.Message);
				stream = null;
				return null;
			}
		}

		private void InjectMergeVariables(Job job)
		{
			if (_globalMergeVariables == null)
				return;

			var jobMergeVariables = new List<MergeVariable>(job.MergeVariables);
			foreach (var mv in _globalMergeVariables)
			{
				if (jobMergeVariables.Any(m => m.Key.Equals(mv.Key, StringComparison.InvariantCultureIgnoreCase)))
					continue; //keep override in job

				jobMergeVariables.Add(mv);
			}
			job.MergeVariables = jobMergeVariables.ToArray();
		}

		private void AdjustTemplatePath(Job job)
		{
			job.Template = Environment.ExpandEnvironmentVariables(job.Template);
			if (string.IsNullOrWhiteSpace(Settings.Default.TemplatesRoot))
				return;
			if (Path.IsPathRooted(job.Template))
				return;
			job.Template = new FileInfo(Path.Combine(Settings.Default.TemplatesRoot, job.Template)).FullName;
		}

		private void ExecuteJob(Job job)
		{
			using (var scope = Program.Container.BeginLifetimeScope(job.JobSource))
			{
				var mgr = scope.Resolve<ConfigurationManager>();
				var config = mgr.LoadAndParse(job);
				mgr.Apply(job, config, false);
			}
		}

		private void CleanupSuccessfulJob(Job job)
		{
			try
			{
				MoveAllFiles(job, Settings.Default.ProcessedFolder);
			}
			catch (IOException)
			{
				Thread.Sleep(500); //one more try, before we give up
				MoveAllFiles(job, Settings.Default.ProcessedFolder);
			}
		}

		private void CleanupAndLogFailedJob(Job job, Exception ex)
		{
			job.TraceSource.TraceEvent(TraceEventType.Critical, WellKnownEventIds.JobFailed, Resources.JobFailed,
				ex.GetType().Name, ex.ToString());
			job.TraceSource.Flush();
			job.TraceSource.Close();

			MoveAllFiles(job, Settings.Default.ErrorFolder);
		}

		private void MoveAllFiles(Job job, string targetFolder)
		{
			var dir = EnsureDirectoryExists(targetFolder);

			//move JOB file itself
			var targetFile = Path.ChangeExtension(Path.GetFileName(job.JobSource), Settings.Default.ProcessedExtension);
			targetFile = Path.Combine(dir.FullName, targetFile);
			targetFile = job.AddJobIdToFilename(targetFile);
			File.Move(job.JobSource, targetFile);

			//now move all other files of the job
			string fileRoot = Path.GetFileNameWithoutExtension(targetFile);
			// ReSharper disable AssignNullToNotNullAttribute
			var sourceFiles = new DirectoryInfo(Path.GetDirectoryName(job.JobSource)).GetFiles(fileRoot + ".*");
			// ReSharper restore AssignNullToNotNullAttribute
			foreach (var file in sourceFiles)
				File.Move(file.FullName, Path.Combine(targetFolder, file.Name));
		}

		/// <summary>
		/// Traces an event
		/// </summary>
		/// <param name="eventId"><see cref="WellKnownEventIds"/></param>
		/// <param name="message">A message with details about the event</param>
		/// <param name="args">Optional parameters of the <paramref name="message"/></param>
		protected virtual void TraceEvent(int eventId, string message, params object[] args)
		{
			var eventType = TraceEventType.Verbose;
			if (eventId >= 4000) eventType = TraceEventType.Critical;
			else if (eventId >= 3000) eventType = TraceEventType.Error;
			else if (eventId >= 2000) eventType = TraceEventType.Warning;
			else if (eventId >= 1000) eventType = TraceEventType.Information;

			TraceEvent(eventType, eventId, message, args);
		}

		/// <summary>
		/// Traces an event
		/// </summary>
		/// <param name="eventType">The type of event to trace</param>
		/// <param name="eventId"><see cref="WellKnownEventIds"/></param>
		/// <param name="message">A message with details about the event</param>
		/// <param name="args">Optional parameters of the <paramref name="message"/></param>
		protected virtual void TraceEvent(TraceEventType eventType, int eventId, string message, object[] args)
		{
			var msg = string.Format(message, args ?? new object[0]);
			TraceSource.TraceEvent(eventType, eventId, msg);

			var eventLogType = GetEventLogEntryType(eventType);
			if (eventLogType.HasValue)
				eventLog.WriteEntry(msg, eventLogType.Value, eventId);
		}

		private EventLogEntryType? GetEventLogEntryType(TraceEventType eventType)
		{
			switch (eventType)
			{
				case TraceEventType.Critical:
				case TraceEventType.Error:
					return EventLogEntryType.Error;
				case TraceEventType.Warning:
					return EventLogEntryType.Warning;
				case TraceEventType.Information:
					return EventLogEntryType.Information;
				//case TraceEventType.Verbose:
				//case TraceEventType.Start:
				//case TraceEventType.Stop:
				//case TraceEventType.Suspend:
				//case TraceEventType.Resume:
				//case TraceEventType.Transfer:
				default:
					return null;
			}
		}

		private void OnRetry(object state)
		{
			new Task(ProcessExistingFiles).Start();
		}

		private void OnConfigChanged(object sender, FileSystemEventArgs e)
		{
			TraceEvent(WellKnownEventIds.ConfigurationChanged, Resources.ConfigurationFileChanged,
								 e.FullPath);
			Settings.Default.Reload();
			try
			{
				ApplyConfiguration();
			}
			catch (Exception ex)
			{
				TraceSource.TraceEvent(TraceEventType.Critical, WellKnownEventIds.ConfigurationChanged, Resources.ConfigurationError, ex.Message);
				TraceSource.TraceEvent(TraceEventType.Verbose, WellKnownEventIds.ConfigurationChanged, "{0}", ex);
				StopService();
				throw;
			}
		}
	}
}
